diff options
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt')
-rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt | 1064 |
1 files changed, 1064 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt new file mode 100644 index 000000000..c9f5797ac --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt @@ -0,0 +1,1064 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.overlay + +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Point +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.VectorDrawable +import android.os.Build +import android.util.AttributeSet +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.SurfaceView +import android.view.View +import android.view.View.OnTouchListener +import android.view.WindowInsets +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import androidx.window.layout.WindowMetricsCalculator +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.NativeLibrary.ButtonType +import org.yuzu.yuzu_emu.NativeLibrary.StickType +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings +import kotlin.math.max +import kotlin.math.min + +/** + * Draws the interactive input overlay on top of the + * [SurfaceView] that is rendering emulation. + */ +class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs), + OnTouchListener { + private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet() + private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet() + private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet() + + private var inEditMode = false + private var buttonBeingConfigured: InputOverlayDrawableButton? = null + private var dpadBeingConfigured: InputOverlayDrawableDpad? = null + private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null + + private lateinit var windowInsets: WindowInsets + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + + windowInsets = rootWindowInsets + + if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { + defaultOverlay() + } + + // Load the controls. + refreshControls() + + // Set the on touch listener. + setOnTouchListener(this) + + // Force draw + setWillNotDraw(false) + + // Request focus for the overlay so it has priority on presses. + requestFocus() + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + for (button in overlayButtons) { + button.draw(canvas) + } + for (dpad in overlayDpads) { + dpad.draw(canvas) + } + for (joystick in overlayJoysticks) { + joystick.draw(canvas) + } + } + + override fun onTouch(v: View, event: MotionEvent): Boolean { + if (inEditMode) { + return onTouchWhileEditing(event) + } + + var shouldUpdateView = false + val playerIndex = + if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device + + for (button in overlayButtons) { + if (!button.updateStatus(event)) { + continue + } + NativeLibrary.onGamePadButtonEvent( + playerIndex, + button.buttonId, + button.status + ) + playHaptics(event) + shouldUpdateView = true + } + + for (dpad in overlayDpads) { + if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide)) { + continue + } + NativeLibrary.onGamePadButtonEvent( + playerIndex, + dpad.upId, + dpad.upStatus + ) + NativeLibrary.onGamePadButtonEvent( + playerIndex, + dpad.downId, + dpad.downStatus + ) + NativeLibrary.onGamePadButtonEvent( + playerIndex, + dpad.leftId, + dpad.leftStatus + ) + NativeLibrary.onGamePadButtonEvent( + playerIndex, + dpad.rightId, + dpad.rightStatus + ) + playHaptics(event) + shouldUpdateView = true + } + + for (joystick in overlayJoysticks) { + if (!joystick.updateStatus(event)) { + continue + } + val axisID = joystick.joystickId + NativeLibrary.onGamePadJoystickEvent( + playerIndex, + axisID, + joystick.xAxis, + joystick.realYAxis + ) + NativeLibrary.onGamePadButtonEvent( + playerIndex, + joystick.buttonId, + joystick.buttonStatus + ) + playHaptics(event) + shouldUpdateView = true + } + + if (shouldUpdateView) + invalidate() + + if (!preferences.getBoolean(Settings.PREF_TOUCH_ENABLED, true)) { + return true + } + + val pointerIndex = event.actionIndex + val xPosition = event.getX(pointerIndex).toInt() + val yPosition = event.getY(pointerIndex).toInt() + val pointerId = event.getPointerId(pointerIndex) + val motionEvent = event.action and MotionEvent.ACTION_MASK + val isActionDown = + motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN + val isActionMove = motionEvent == MotionEvent.ACTION_MOVE + val isActionUp = + motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP + + if (isActionDown && !isTouchInputConsumed(pointerId)) { + NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) + } + + if (isActionMove) { + for (i in 0 until event.pointerCount) { + val fingerId = event.getPointerId(i) + if (isTouchInputConsumed(fingerId)) { + continue + } + NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)) + } + } + + if (isActionUp && !isTouchInputConsumed(pointerId)) { + NativeLibrary.onTouchReleased(pointerId) + } + + return true + } + + private fun playHaptics(event: MotionEvent) { + if (EmulationMenuSettings.hapticFeedback) { + when (event.actionMasked) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE) + } + } + } + + private fun isTouchInputConsumed(track_id: Int): Boolean { + for (button in overlayButtons) { + if (button.trackId == track_id) { + return true + } + } + for (dpad in overlayDpads) { + if (dpad.trackId == track_id) { + return true + } + } + for (joystick in overlayJoysticks) { + if (joystick.trackId == track_id) { + return true + } + } + return false + } + + private fun onTouchWhileEditing(event: MotionEvent): Boolean { + val pointerIndex = event.actionIndex + val fingerPositionX = event.getX(pointerIndex).toInt() + val fingerPositionY = event.getY(pointerIndex).toInt() + + // TODO: Provide support for portrait layout + //val orientation = + // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" + + for (button in overlayButtons) { + // Determine the button state to apply based on the MotionEvent action flag. + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> + // If no button is being moved now, remember the currently touched button to move. + if (buttonBeingConfigured == null && + button.bounds.contains( + fingerPositionX, + fingerPositionY + ) + ) { + buttonBeingConfigured = button + buttonBeingConfigured!!.onConfigureTouch(event) + } + + MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) { + buttonBeingConfigured!!.onConfigureTouch(event) + invalidate() + return true + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) { + // Persist button position by saving new place. + saveControlPosition( + buttonBeingConfigured!!.buttonId, + buttonBeingConfigured!!.bounds.centerX(), + buttonBeingConfigured!!.bounds.centerY(), + "" + ) + buttonBeingConfigured = null + } + } + } + + for (dpad in overlayDpads) { + // Determine the button state to apply based on the MotionEvent action flag. + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> + // If no button is being moved now, remember the currently touched button to move. + if (buttonBeingConfigured == null && + dpad.bounds.contains(fingerPositionX, fingerPositionY) + ) { + dpadBeingConfigured = dpad + dpadBeingConfigured!!.onConfigureTouch(event) + } + + MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) { + dpadBeingConfigured!!.onConfigureTouch(event) + invalidate() + return true + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) { + // Persist button position by saving new place. + saveControlPosition( + dpadBeingConfigured!!.upId, + dpadBeingConfigured!!.bounds.centerX(), + dpadBeingConfigured!!.bounds.centerY(), + "" + ) + dpadBeingConfigured = null + } + } + } + + for (joystick in overlayJoysticks) { + when (event.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null && + joystick.bounds.contains( + fingerPositionX, + fingerPositionY + ) + ) { + joystickBeingConfigured = joystick + joystickBeingConfigured!!.onConfigureTouch(event) + } + + MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) { + joystickBeingConfigured!!.onConfigureTouch(event) + invalidate() + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) { + saveControlPosition( + joystickBeingConfigured!!.buttonId, + joystickBeingConfigured!!.bounds.centerX(), + joystickBeingConfigured!!.bounds.centerY(), + "" + ) + joystickBeingConfigured = null + } + } + } + + return true + } + + private fun addOverlayControls(orientation: String) { + val windowSize = getSafeScreenSize(context) + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_a, + R.drawable.facebutton_a_depressed, + ButtonType.BUTTON_A, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_1, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_b, + R.drawable.facebutton_b_depressed, + ButtonType.BUTTON_B, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_2, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_x, + R.drawable.facebutton_x_depressed, + ButtonType.BUTTON_X, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_3, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_y, + R.drawable.facebutton_y_depressed, + ButtonType.BUTTON_Y, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_4, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.l_shoulder, + R.drawable.l_shoulder_depressed, + ButtonType.TRIGGER_L, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_5, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.r_shoulder, + R.drawable.r_shoulder_depressed, + ButtonType.TRIGGER_R, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_6, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.zl_trigger, + R.drawable.zl_trigger_depressed, + ButtonType.TRIGGER_ZL, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_7, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.zr_trigger, + R.drawable.zr_trigger_depressed, + ButtonType.TRIGGER_ZR, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_8, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_plus, + R.drawable.facebutton_plus_depressed, + ButtonType.BUTTON_PLUS, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_9, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_minus, + R.drawable.facebutton_minus_depressed, + ButtonType.BUTTON_MINUS, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_10, true)) { + overlayDpads.add( + initializeOverlayDpad( + context, + windowSize, + R.drawable.dpad_standard, + R.drawable.dpad_standard_cardinal_depressed, + R.drawable.dpad_standard_diagonal_depressed, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_11, true)) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + windowSize, + R.drawable.joystick_range, + R.drawable.joystick, + R.drawable.joystick_depressed, + StickType.STICK_L, + ButtonType.STICK_L, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_12, true)) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + windowSize, + R.drawable.joystick_range, + R.drawable.joystick, + R.drawable.joystick_depressed, + StickType.STICK_R, + ButtonType.STICK_R, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_13, false)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_home, + R.drawable.facebutton_home_depressed, + ButtonType.BUTTON_HOME, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_14, false)) { + overlayButtons.add( + initializeOverlayButton( + context, + windowSize, + R.drawable.facebutton_screenshot, + R.drawable.facebutton_screenshot_depressed, + ButtonType.BUTTON_CAPTURE, + orientation + ) + ) + } + } + + fun refreshControls() { + // Remove all the overlay buttons from the HashSet. + overlayButtons.clear() + overlayDpads.clear() + overlayJoysticks.clear() + val orientation = + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" + + // Add all the enabled overlay items back to the HashSet. + if (EmulationMenuSettings.showOverlay) { + addOverlayControls(orientation) + } + invalidate() + } + + private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) { + val windowSize = getSafeScreenSize(context) + val min = windowSize.first + val max = windowSize.second + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() + .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x) + .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y) + .apply() + } + + fun setIsInEditMode(editMode: Boolean) { + inEditMode = editMode + } + + private fun defaultOverlay() { + if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { + defaultOverlayLandscape() + } + + resetButtonPlacement() + preferences.edit() + .putBoolean(Settings.PREF_OVERLAY_INIT, true) + .apply() + } + + fun resetButtonPlacement() { + defaultOverlayLandscape() + refreshControls() + } + + private fun defaultOverlayLandscape() { + // Each value represents the position of the button in relation to the screen size without insets. + preferences.edit() + .putFloat( + ButtonType.BUTTON_A.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_A.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_B.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_B.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_X.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_X.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_ZL.toString() + "-X", + resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_ZL.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_ZR.toString() + "-X", + resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_ZR.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.DPAD_UP.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.DPAD_UP.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + "-X", + resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + "-X", + resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_PLUS.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_PLUS.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_MINUS.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_MINUS.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_HOME.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_HOME.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_CAPTURE.toString() + "-X", + resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) + .toFloat() / 1000 + ) + .putFloat( + ButtonType.BUTTON_CAPTURE.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) + .toFloat() / 1000 + ) + .putFloat( + ButtonType.STICK_R.toString() + "-X", + resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.STICK_R.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 + ) + .putFloat( + ButtonType.STICK_L.toString() + "-X", + resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 + ) + .putFloat( + ButtonType.STICK_L.toString() + "-Y", + resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 + ) + .apply() + } + + override fun isInEditMode(): Boolean { + return inEditMode + } + + companion object { + private val preferences: SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + /** + * Resizes a [Bitmap] by a given scale factor + * + * @param context Context for getting the vector drawable + * @param drawableId The ID of the drawable to scale. + * @param scale The scale factor for the bitmap. + * @return The scaled [Bitmap] + */ + private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap { + val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable + + val bitmap = Bitmap.createBitmap( + (vectorDrawable.intrinsicWidth * scale).toInt(), + (vectorDrawable.intrinsicHeight * scale).toInt(), + Bitmap.Config.ARGB_8888 + ) + + val dm = context.resources.displayMetrics + val minScreenDimension = min(dm.widthPixels, dm.heightPixels) + + val maxBitmapDimension = max(bitmap.width, bitmap.height) + val bitmapScale = scale * minScreenDimension / maxBitmapDimension + + val scaledBitmap = Bitmap.createScaledBitmap( + bitmap, + (bitmap.width * bitmapScale).toInt(), + (bitmap.height * bitmapScale).toInt(), + true + ) + + val canvas = Canvas(scaledBitmap) + vectorDrawable.setBounds(0, 0, canvas.width, canvas.height) + vectorDrawable.draw(canvas) + return scaledBitmap + } + + /** + * Gets the safe screen size for drawing the overlay + * + * @param context Context for getting the window metrics + * @return A pair of points, the first being the top left corner of the safe area, + * the second being the bottom right corner of the safe area + */ + private fun getSafeScreenSize(context: Context): Pair<Point, Point> { + // Get screen size + val windowMetrics = + WindowMetricsCalculator.getOrCreate() + .computeCurrentWindowMetrics(context as Activity) + var maxY = windowMetrics.bounds.height().toFloat() + var maxX = windowMetrics.bounds.width().toFloat() + var minY = 0 + var minX = 0 + + // If we have API access, calculate the safe area to draw the overlay + var cutoutLeft = 0 + var cutoutBottom = 0 + val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout + if (insets != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2) + insets.boundingRectTop.bottom.toFloat() else maxY + if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2) + insets.boundingRectRight.left.toFloat() else maxX + + minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left + minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom + + cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left + cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom + } + + // This makes sure that if we have an inset on one side of the screen, we mirror it on + // the other side. Since removing space from one of the max values messes with the scale, + // we also have to account for it using our min values. + if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft + if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom + if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) { + maxX -= (minX * 2) + } else if (minX > 0) { + maxX -= minX + } + if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) { + maxY -= (minY * 2) + } else if (minY > 0) { + maxY -= minY + } + + return Pair(Point(minX, minY), Point(maxX.toInt(), maxY.toInt())) + } + + /** + * Initializes an InputOverlayDrawableButton, given by resId, with all of the + * parameters set for it to be properly shown on the InputOverlay. + * + * + * This works due to the way the X and Y coordinates are stored within + * the [SharedPreferences]. + * + * + * In the input overlay configuration menu, + * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). + * the X and Y coordinates of the button at the END of its touch event + * (when you remove your finger/stylus from the touchscreen) are then stored + * within a SharedPreferences instance so that those values can be retrieved here. + * + * + * This has a few benefits over the conventional way of storing the values + * (ie. within the yuzu ini file). + * + * * No native calls + * * Keeps Android-only values inside the Android environment + * + * + * + * Technically no modifications should need to be performed on the returned + * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait + * for Android to call the onDraw method. + * + * @param context The current [Context]. + * @param windowSize The size of the window to draw the overlay on. + * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State). + * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State). + * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. + * @return An [InputOverlayDrawableButton] with the correct drawing bounds set. + */ + private fun initializeOverlayButton( + context: Context, + windowSize: Pair<Point, Point>, + defaultResId: Int, + pressedResId: Int, + buttonId: Int, + orientation: String + ): InputOverlayDrawableButton { + // Resources handle for fetching the initial Drawable resource. + val res = context.resources + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. + val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + // Decide scale based on button ID and user preference + var scale: Float = when (buttonId) { + ButtonType.BUTTON_HOME, + ButtonType.BUTTON_CAPTURE, + ButtonType.BUTTON_PLUS, + ButtonType.BUTTON_MINUS -> 0.07f + + ButtonType.TRIGGER_L, + ButtonType.TRIGGER_R, + ButtonType.TRIGGER_ZL, + ButtonType.TRIGGER_ZR -> 0.26f + + else -> 0.11f + } + scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableButton. + val defaultStateBitmap = getBitmap(context, defaultResId, scale) + val pressedStateBitmap = getBitmap(context, pressedResId, scale) + val overlayDrawable = + InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId) + + // Get the minimum and maximum coordinates of the screen where the button can be placed. + val min = windowSize.first + val max = windowSize.second + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + val xKey = "$buttonId$orientation-X" + val yKey = "$buttonId$orientation-Y" + val drawableXPercent = sPrefs.getFloat(xKey, 0f) + val drawableYPercent = sPrefs.getFloat(yKey, 0f) + val drawableX = (drawableXPercent * max.x + min.x).toInt() + val drawableY = (drawableYPercent * max.y + min.y).toInt() + val width = overlayDrawable.width + val height = overlayDrawable.height + + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. + overlayDrawable.setBounds( + drawableX - (width / 2), + drawableY - (height / 2), + drawableX + (width / 2), + drawableY + (height / 2) + ) + + // Need to set the image's position + overlayDrawable.setPosition( + drawableX - (width / 2), + drawableY - (height / 2) + ) + val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100) + overlayDrawable.setOpacity(savedOpacity * 255 / 100) + return overlayDrawable + } + + /** + * Initializes an [InputOverlayDrawableDpad] + * + * @param context The current [Context]. + * @param windowSize The size of the window to draw the overlay on. + * @param defaultResId The [Bitmap] resource ID of the default state. + * @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction. + * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions. + * @return the initialized [InputOverlayDrawableDpad] + */ + private fun initializeOverlayDpad( + context: Context, + windowSize: Pair<Point, Point>, + defaultResId: Int, + pressedOneDirectionResId: Int, + pressedTwoDirectionsResId: Int, + orientation: String + ): InputOverlayDrawableDpad { + // Resources handle for fetching the initial Drawable resource. + val res = context.resources + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. + val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + // Decide scale based on button ID and user preference + var scale = 0.25f + scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableDpad. + val defaultStateBitmap = + getBitmap(context, defaultResId, scale) + val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale) + val pressedTwoDirectionsStateBitmap = + getBitmap(context, pressedTwoDirectionsResId, scale) + + val overlayDrawable = InputOverlayDrawableDpad( + res, + defaultStateBitmap, + pressedOneDirectionStateBitmap, + pressedTwoDirectionsStateBitmap, + ButtonType.DPAD_UP, + ButtonType.DPAD_DOWN, + ButtonType.DPAD_LEFT, + ButtonType.DPAD_RIGHT + ) + + // Get the minimum and maximum coordinates of the screen where the button can be placed. + val min = windowSize.first + val max = windowSize.second + + // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. + // These were set in the input overlay configuration menu. + val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f) + val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f) + val drawableX = (drawableXPercent * max.x + min.x).toInt() + val drawableY = (drawableYPercent * max.y + min.y).toInt() + val width = overlayDrawable.width + val height = overlayDrawable.height + + // Now set the bounds for the InputOverlayDrawableDpad. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. + overlayDrawable.setBounds( + drawableX - (width / 2), + drawableY - (height / 2), + drawableX + (width / 2), + drawableY + (height / 2) + ) + + // Need to set the image's position + overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2)) + val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100) + overlayDrawable.setOpacity(savedOpacity * 255 / 100) + return overlayDrawable + } + + /** + * Initializes an [InputOverlayDrawableJoystick] + * + * @param context The current [Context] + * @param windowSize The size of the window to draw the overlay on. + * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). + * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). + * @param pressedResInner Resource ID for the pressed inner image of the joystick. + * @param joystick Identifier for which joystick this is. + * @param button Identifier for which joystick button this is. + * @return the initialized [InputOverlayDrawableJoystick]. + */ + private fun initializeOverlayJoystick( + context: Context, + windowSize: Pair<Point, Point>, + resOuter: Int, + defaultResInner: Int, + pressedResInner: Int, + joystick: Int, + button: Int, + orientation: String + ): InputOverlayDrawableJoystick { + // Resources handle for fetching the initial Drawable resource. + val res = context.resources + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. + val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + // Decide scale based on user preference + var scale = 0.3f + scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableJoystick. + val bitmapOuter = getBitmap(context, resOuter, scale) + val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f) + val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f) + + // Get the minimum and maximum coordinates of the screen where the button can be placed. + val min = windowSize.first + val max = windowSize.second + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f) + val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f) + val drawableX = (drawableXPercent * max.x + min.x).toInt() + val drawableY = (drawableYPercent * max.y + min.y).toInt() + val outerScale = 1.66f + + // Now set the bounds for the InputOverlayDrawableJoystick. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. + val outerSize = bitmapOuter.width + val outerRect = Rect( + drawableX - (outerSize / 2), + drawableY - (outerSize / 2), + drawableX + (outerSize / 2), + drawableY + (outerSize / 2) + ) + val innerRect = + Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt()) + + // Send the drawableId to the joystick so it can be referenced when saving control position. + val overlayDrawable = InputOverlayDrawableJoystick( + res, + bitmapOuter, + bitmapInnerDefault, + bitmapInnerPressed, + outerRect, + innerRect, + joystick, + button + ) + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY) + val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100) + overlayDrawable.setOpacity(savedOpacity * 255 / 100) + return overlayDrawable + } + } +} |